package com.akdeniz.googleplaycrawler.cli;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.WriteFuture;
import org.apache.mina.core.session.IoSession;

import com.akdeniz.googleplaycrawler.GooglePlayAPI;
import com.akdeniz.googleplaycrawler.GooglePlayAPI.RECOMMENDATION_TYPE;
import com.akdeniz.googleplaycrawler.GooglePlayAPI.REVIEW_SORT;
import com.akdeniz.googleplaycrawler.GooglePlayException;
import com.akdeniz.googleplaycrawler.GooglePlay.AppDetails;
import com.akdeniz.googleplaycrawler.GooglePlay.BrowseLink;
import com.akdeniz.googleplaycrawler.GooglePlay.BrowseResponse;
import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsEntry;
import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsResponse;
import com.akdeniz.googleplaycrawler.GooglePlay.DetailsResponse;
import com.akdeniz.googleplaycrawler.GooglePlay.DocV2;
import com.akdeniz.googleplaycrawler.GooglePlay.GetReviewsResponse;
import com.akdeniz.googleplaycrawler.GooglePlay.ListResponse;
import com.akdeniz.googleplaycrawler.GooglePlay.Offer;
import com.akdeniz.googleplaycrawler.GooglePlay.ReviewResponse;
import com.akdeniz.googleplaycrawler.GooglePlay.SearchResponse;
import com.akdeniz.googleplaycrawler.gsf.GoogleServicesFramework.BindAccountResponse;
import com.akdeniz.googleplaycrawler.gsf.GoogleServicesFramework.LoginResponse;
import com.akdeniz.googleplaycrawler.gsf.packets.BindAccountRequestPacket;
import com.akdeniz.googleplaycrawler.gsf.packets.HeartBeatPacket;
import com.akdeniz.googleplaycrawler.gsf.packets.LoginRequestPacket;
import com.akdeniz.googleplaycrawler.gsf.MTalkConnector;
import com.akdeniz.googleplaycrawler.gsf.MessageFilter;
import com.akdeniz.googleplaycrawler.gsf.NotificationListener;
import com.akdeniz.googleplaycrawler.Utils;

import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.impl.choice.CollectionArgumentChoice;
import net.sourceforge.argparse4j.inf.Argument;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.ArgumentType;
import net.sourceforge.argparse4j.inf.FeatureControl;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import net.sourceforge.argparse4j.inf.Subparsers;

/**
 * 
 * @author akdeniz
 *
 */
public class googleplay {

	private static final String DELIMETER = ";";

	private ArgumentParser parser;
	private GooglePlayAPI service;
	private Namespace namespace;

	public static enum COMMAND {
		LIST, DOWNLOAD, CHECKIN, CATEGORIES, SEARCH, PERMISSIONS, REVIEWS, REGISTER, USEGCM, RECOMMENDATIONS
	}

	private static final String LIST_HEADER = new StringJoiner(DELIMETER)
			.add("Title").add("Package").add("Creator").add("Price")
			.add("Installation Size").add("Number Of Downloads").toString();
	private static final String CATEGORIES_HEADER = new StringJoiner(DELIMETER)
			.add("ID").add("Name").toString();
	private static final String SUBCATEGORIES_HEADER = new StringJoiner(
			DELIMETER).add("ID").add("Title").toString();

	private static final int TIMEOUT = 10000;

	public googleplay() {
		parser = ArgumentParsers.newArgumentParser("googleplay").description(
				"Play with Google Play API :)");

		/* =================Common Arguments============== */
		parser.addArgument("-f", "--conf")
				.nargs("?")
				.help("Configuration file to used for login! If any of androidid, email and password is supplied, it will be ignored!")
				.setDefault(FeatureControl.SUPPRESS);
		parser.addArgument("-i", "--androidid")
				.nargs("?")
				.help("ANDROID-ID to be used! You can use \"Checkin\" mechanism, if you don't have one!")
				.setDefault(FeatureControl.SUPPRESS);
		parser.addArgument("-e", "--email").nargs("?")
				.help("Email address to be used for login.")
				.setDefault(FeatureControl.SUPPRESS);
		parser.addArgument("-p", "--password").nargs("?")
				.help("Password to be used for login.")
				.setDefault(FeatureControl.SUPPRESS);
		parser.addArgument("-t", "--securitytoken")
				.nargs("?")
				.help("Security token that was generated at checkin. It is only required for \"usegcm\" option")
				.setDefault(FeatureControl.SUPPRESS);
		parser.addArgument("-z", "--localization")
				.nargs("?")
				.help("Localization string that will customise fetched informations such as reviews, "
						+ "descriptions,... Can be : en-EN, en-US, tr-TR, fr-FR ... (default : en-EN)")
				.setDefault(FeatureControl.SUPPRESS);
		parser.addArgument("-a", "--host").nargs("?").help("Proxy host")
				.setDefault(FeatureControl.SUPPRESS);
		parser.addArgument("-l", "--port").type(Integer.class).nargs("?")
				.help("Proxy port").setDefault(FeatureControl.SUPPRESS);

		Subparsers subparsers = parser.addSubparsers().description(
				"Command to be executed.");

		/* =================Download Arguments============== */
		Subparser downloadParser = subparsers.addParser("download", true)
				.description("download file(s)!")
				.setDefault("command", COMMAND.DOWNLOAD);
		downloadParser.addArgument("packagename").nargs("+")
				.help("applications to download");

		/* =================Check-In Arguments============== */
		subparsers.addParser("checkin", true).description("checkin section!")
				.setDefault("command", COMMAND.CHECKIN);

		/* =================List Arguments============== */
		Subparser listParser = subparsers
				.addParser("list", true)
				.description(
						"Lists sub-categories and applications within them!")
				.setDefault("command", COMMAND.LIST);
		listParser.addArgument("category").required(true)
				.help("defines category");
		listParser.addArgument("-s", "--subcategory").required(false)
				.help("defines sub-category");
		listParser.addArgument("-o", "--offset").type(Integer.class)
				.required(false).help("offset to define where list begins");
		listParser.addArgument("-n", "--number").type(Integer.class)
				.required(false).help("how many app will be listed");

		/* =================Categories Arguments============== */
		Subparser categoriesParser = subparsers.addParser("categories", true)
				.description("list categories for browse section")
				.setDefault("command", COMMAND.CATEGORIES);

		/* =================Search Arguments============== */
		Subparser searchParser = subparsers.addParser("search", true)
				.description("search for query!")
				.setDefault("command", COMMAND.SEARCH);
		searchParser.addArgument("query").help("query to be searched");
		searchParser.addArgument("-o", "--offset").type(Integer.class)
				.required(false).help("offset to define where list begins");
		searchParser.addArgument("-n", "--number").type(Integer.class)
				.required(false).help("how many app will be listed");

		/* =================Permissions Arguments============== */
		Subparser permissionsParser = subparsers.addParser("permissions", true)
				.description("list permissions of given application")
				.setDefault("command", COMMAND.PERMISSIONS);
		permissionsParser.addArgument("package").nargs("+")
				.help("applications whose permissions to be listed");

		/* =================Reviews Arguments============== */
		Subparser reviewsParser = subparsers.addParser("reviews", true)
				.description("lists reviews of given application")
				.setDefault("command", COMMAND.REVIEWS);
		reviewsParser.addArgument("package").help(
				"application whose reviews to be listed");
		reviewsParser.addArgument("-s", "--sort")
				.choices(new ReviewSortChoice()).type(new ReviewSort())
				.required(false).help("sorting type")
				.setDefault(REVIEW_SORT.HELPFUL);
		reviewsParser.addArgument("-o", "--offset").type(Integer.class)
				.required(false).help("offset to define where list begins");
		reviewsParser.addArgument("-n", "--number").type(Integer.class)
				.required(false).help("how many reviews will be listed");

		/* =================Recommendation Arguments============== */
		Subparser recommendationParser = subparsers
				.addParser("recommendations", true)
				.description("lists recommended apps of given application")
				.setDefault("command", COMMAND.RECOMMENDATIONS);
		recommendationParser.addArgument("package").help(
				"application whose recommendations to be listed");
		recommendationParser.addArgument("-t", "--type")
				.choices(new ReleationChoice()).type(new RecommendationType())
				.required(false).help("releations type")
				.setDefault(RECOMMENDATION_TYPE.ALSO_INSTALLED);
		recommendationParser.addArgument("-o", "--offset").type(Integer.class)
				.required(false).help("offset to define where list begins");
		recommendationParser.addArgument("-n", "--number").type(Integer.class)
				.required(false)
				.help("how many recommendations will be listed");

		/* =================Register Arguments============== */
		subparsers.addParser("register", true)
				.description("registers device so that can be seen from web!")
				.setDefault("command", COMMAND.REGISTER);

		/* =================UseGCM Arguments============== */
		subparsers
				.addParser("usegcm", true)
				.description(
						"listens GCM(GoogleCloudMessaging) for download notification and downloads them!")
				.setDefault("command", COMMAND.USEGCM);
	}

	public static void main(String[] args) throws Exception {

		new googleplay().operate(args);
	}

	public void operate(String[] argv) {
		try {
			namespace = parser.parseArgs(argv);
		} catch (ArgumentParserException e) {
			System.err.println(e.getMessage());
			parser.printHelp();
			System.exit(-1);
		}

		COMMAND command = (COMMAND) namespace.get("command");

		try {
			switch (command) {
			case CHECKIN:
				checkinCommand();
				break;
			case DOWNLOAD:
				downloadCommand();
				break;
			case LIST:
				listCommand();
				break;
			case CATEGORIES:
				categoriesCommand();
				break;
			case SEARCH:
				searchCommand();
				break;
			case PERMISSIONS:
				permissionsCommand();
				break;
			case REVIEWS:
				reviewsCommand();
				break;
			case REGISTER:
				registerCommand();
				break;
			case USEGCM:
				useGCMCommand();
				break;
			case RECOMMENDATIONS:
				recommendationsCommand();
				break;
			}
		} catch (Exception e) {
			System.err.println(e.getMessage());
			System.exit(-1);
		}
	}

	private void useGCMCommand() throws Exception {
		String ac2dmAuth = loginAC2DM();

		MTalkConnector connector = new MTalkConnector(new NotificationListener(
				service));
		ConnectFuture connectFuture = connector.connect();
		connectFuture.await(TIMEOUT);
		if (!connectFuture.isConnected()) {
			throw new IOException("Couldn't connect to GTALK server!");
		}

		final IoSession session = connectFuture.getSession();
		send(session, IoBuffer.wrap(new byte[] { 0x07 })); // connection sanity
															// check
		System.out.println("Connected to server.");

		String deviceIDStr = String.valueOf(new BigInteger(service
				.getAndroidID(), 16).longValue());
		String securityTokenStr = String.valueOf(new BigInteger(service
				.getSecurityToken(), 16).longValue());

		LoginRequestPacket loginRequestPacket = new LoginRequestPacket(
				deviceIDStr, securityTokenStr, service.getAndroidID());

		LoginResponseFilter loginResponseFilter = new LoginResponseFilter(
				loginRequestPacket.getPacketID());
		connector.addFilter(loginResponseFilter);
		send(session, loginRequestPacket);
		LoginResponse loginResponse = loginResponseFilter.nextMessage(TIMEOUT);
		connector.removeFilter(loginResponseFilter);
		if (loginResponse == null) {
			throw new IllegalStateException(
					"Login response could not be received!");
		} else if (loginResponse.hasError()) {
			throw new IllegalStateException(loginResponse.getError()
					.getExtension(0).getMessage());
		}
		System.out.println("Autheticated.");

		BindAccountRequestPacket bindAccountRequestPacket = new BindAccountRequestPacket(
				service.getEmail(), ac2dmAuth);

		BindAccountResponseFilter barf = new BindAccountResponseFilter(
				bindAccountRequestPacket.getPacketID());
		connector.addFilter(barf);
		send(session, bindAccountRequestPacket);
		BindAccountResponse bindAccountResponse = barf.nextMessage(TIMEOUT);
		connector.removeFilter(barf);

		/*
		 * if(bindAccountResponse==null){ throw new
		 * IllegalStateException("Account bind response could not be received!"
		 * ); } else if(bindAccountResponse.hasError()){ throw new
		 * IllegalStateException
		 * (bindAccountResponse.getError().getExtension(0).getMessage()); }
		 */

		System.out.println("Listening for notifications from server..");

		// send heart beat packets to keep connection up.
		while (true) {
			send(session, new HeartBeatPacket());
			Thread.sleep(30000);
		}
	}

	private static void send(IoSession session, Object object)
			throws InterruptedException, IOException {
		WriteFuture writeFuture = session.write(object);
		writeFuture.await(TIMEOUT);
		if (!writeFuture.isWritten()) {
			Throwable exception = writeFuture.getException();
			if (exception != null) {
				throw new IOException("Error occured while writing!", exception);
			}
			throw new IOException("Error occured while writing!");
		}
	}

	private void recommendationsCommand() throws Exception {
		login();

		String packageName = namespace.getString("package");
		RECOMMENDATION_TYPE type = (RECOMMENDATION_TYPE) namespace.get("type");
		Integer offset = namespace.getInt("offset");
		Integer number = namespace.getInt("number");

		ListResponse recommendations = service.recommendations(packageName,
				type, offset, number);

		if (recommendations.getDoc(0).getChildCount() == 0) {
			System.out.println("No recommendation found!");
		} else {
			for (DocV2 child : recommendations.getDoc(0).getChildList()) {
				System.out.println(child.getDetails().getAppDetails()
						.getPackageName());
			}
		}
	}

	private void reviewsCommand() throws Exception {
		login();

		String packageName = namespace.getString("package");
		REVIEW_SORT sort = (REVIEW_SORT) namespace.get("sort");
		Integer offset = namespace.getInt("offset");
		Integer number = namespace.getInt("number");

		ReviewResponse reviews = service.reviews(packageName, sort, offset,
				number);
		GetReviewsResponse response = reviews.getGetResponse();
		if (response.getReviewCount() == 0) {
			System.out.println("No review found!");
		}
		System.out.println(response);
	}

	private void registerCommand() throws Exception {
		login();
		service.uploadDeviceConfig();
		System.out
				.println("A device is registered to your account! You can see it at \"https://play.google.com/store/account\" after a few downloads!");
	}

	private void permissionsCommand() throws Exception {
		login();

		List<String> packages = namespace.getList("package");
		BulkDetailsResponse bulkDetails = service.bulkDetails(packages);

		for (BulkDetailsEntry bulkDetailsEntry : bulkDetails.getEntryList()) {
			DocV2 doc = bulkDetailsEntry.getDoc();
			AppDetails appDetails = doc.getDetails().getAppDetails();
			System.out.println(doc.getDocid());
			for (String permission : appDetails.getPermissionList()) {
				System.out.println("\t" + permission);
			}
		}

	}

	private void searchCommand() throws Exception {
		login();

		String query = namespace.getString("query");
		Integer offset = namespace.getInt("offset");
		Integer number = namespace.getInt("number");

		SearchResponse searchResponse = service.search(query, offset, number);
		System.out.println(LIST_HEADER);
		for (DocV2 child : searchResponse.getDoc(0).getChildList()) {
			AppDetails appDetails = child.getDetails().getAppDetails();
			String formatted = new StringJoiner(DELIMETER)
					.add(child.getTitle()).add(appDetails.getPackageName())
					.add(child.getCreator())
					.add(child.getOffer(0).getFormattedAmount())
					.add(String.valueOf(appDetails.getInstallationSize()))
					.add(appDetails.getNumDownloads()).toString();
			System.out.println(formatted);

		}
	}

	private void categoriesCommand() throws Exception {
		login();
		BrowseResponse browseResponse = service.browse();
		System.out.println(CATEGORIES_HEADER);
		for (BrowseLink browseLink : browseResponse.getCategoryList()) {
			String[] splitedStrs = browseLink.getDataUrl().split("&cat=");
			System.out.println(new StringJoiner(DELIMETER)
					.add(splitedStrs[splitedStrs.length - 1])
					.add(browseLink.getName()).toString());
		}
	}

	private void checkinCommand() throws Exception {
		checkin();

		System.out.println("Your account succesfully checkined!");
		System.out.println("AndroidID : " + service.getAndroidID());
		System.out.println("SecurityToken : " + service.getSecurityToken());
	}

	private void login() throws Exception {
		String androidid = namespace.getString("androidid");
		String email = namespace.getString("email");
		String password = namespace.getString("password");
		String localization = namespace.getString("localization");

		if (androidid != null && email != null && password != null) {
			createLoginableService(androidid, email, password, localization);
			service.login();
			return;
		}

		if (namespace.getAttrs().containsKey("conf")) {
			Properties properties = new Properties();
			properties.load(new FileInputStream(namespace.getString("conf")));

			androidid = properties.getProperty("androidid");
			email = properties.getProperty("email");
			password = properties.getProperty("password");
			localization = properties.getProperty("localization");

			if (androidid != null && email != null && password != null) {
				createLoginableService(androidid, email, password, localization);
				service.login();
				return;
			}
		}

		throw new GooglePlayException("Lack of information for login!");
	}

	private String loginAC2DM() throws Exception {
		String androidid = namespace.getString("androidid");
		String email = namespace.getString("email");
		String password = namespace.getString("password");
		String securityToken = namespace.getString("securitytoken");
		String localization = namespace.getString("localization");

		if (androidid != null && email != null && password != null
				&& securityToken != null) {
			createLoginableService(androidid, email, password, localization);
			service.login();
			service.setSecurityToken(securityToken);
			return service.loginAC2DM();
		}

		if (namespace.getAttrs().containsKey("conf")) {
			Properties properties = new Properties();
			properties.load(new FileInputStream(namespace.getString("conf")));

			androidid = properties.getProperty("androidid");
			email = properties.getProperty("email");
			password = properties.getProperty("password");
			securityToken = properties.getProperty("securitytoken");
			localization = properties.getProperty("localization");

			if (androidid != null && email != null && password != null
					&& securityToken != null) {
				createLoginableService(androidid, email, password, localization);
				service.login();
				service.setSecurityToken(securityToken);
				return service.loginAC2DM();
			}
		}

		throw new GooglePlayException("Lack of information for login!");
	}

	private void createLoginableService(String androidid, String email,
			String password, String localization) throws Exception {
		service = new GooglePlayAPI(email, password, androidid);
		service.setLocalization(localization);
		HttpClient proxiedHttpClient = getProxiedHttpClient();
		if (proxiedHttpClient != null) {
			service.setClient(proxiedHttpClient);
		}
	}

	private void createCheckinableService(String email, String password,
			String localization) throws Exception {
		service = new GooglePlayAPI(email, password);
		service.setLocalization(localization);
		HttpClient proxiedHttpClient = getProxiedHttpClient();
		if (proxiedHttpClient != null) {
			service.setClient(proxiedHttpClient);
		}
	}

	private void listCommand() throws Exception {
		login();

		String category = namespace.getString("category");
		String subcategory = namespace.getString("subcategory");
		Integer offset = namespace.getInt("offset");
		Integer number = namespace.getInt("number");

		ListResponse listResponse = service.list(category, subcategory, offset,
				number);
		if (subcategory == null) {
			System.out.println(SUBCATEGORIES_HEADER);
			for (DocV2 child : listResponse.getDocList()) {
				String formatted = new StringJoiner(DELIMETER)
						.add(child.getDocid()).add(child.getTitle()).toString();
				System.out.println(formatted);
			}
		} else {
			System.out.println(LIST_HEADER);
			for (DocV2 child : listResponse.getDoc(0).getChildList()) {
				AppDetails appDetails = child.getDetails().getAppDetails();
				String formatted = new StringJoiner(DELIMETER)
						.add(child.getTitle()).add(appDetails.getPackageName())
						.add(child.getCreator())
						.add(child.getOffer(0).getFormattedAmount())
						.add(String.valueOf(appDetails.getInstallationSize()))
						.add(appDetails.getNumDownloads()).toString();
				System.out.println(formatted);

			}
		}
	}

	private void downloadCommand() throws Exception {
		login();
		List<String> packageNames = namespace.getList("packagename");
		for (String packageName : packageNames) {
			download(packageName);
		}
	}

	private void checkin() throws Exception {
		String email = namespace.getString("email");
		String password = namespace.getString("password");
		String localization = namespace.getString("localization");

		if (email != null && password != null) {
			createCheckinableService(email, password, localization);
			service.checkin();
			return;
		}

		if (namespace.getAttrs().containsKey("conf")) {
			Properties properties = new Properties();
			properties.load(new FileInputStream(namespace.getString("conf")));

			email = properties.getProperty("email");
			password = properties.getProperty("password");
			localization = properties.getProperty("localization");

			if (email != null && password != null) {
				createCheckinableService(email, password, localization);
				service.checkin();
				return;
			}
		}

		throw new GooglePlayException("Lack of information for login!");
	}

	private HttpClient getProxiedHttpClient() throws Exception {
		String host = namespace.getString("host");
		Integer port = namespace.getInt("port");

		if (host != null && port != null) {
			return getProxiedHttpClient(host, port);
		}

		if (namespace.getAttrs().containsKey("conf")) {
			Properties properties = new Properties();
			properties.load(new FileInputStream(namespace.getString("conf")));

			host = properties.getProperty("host");
			String portString = properties.getProperty("port");

			if (host != null && portString != null) {
				port = Integer.valueOf(portString);
				return getProxiedHttpClient(host, port);
			}
		}

		return null;
	}

	private static HttpClient getProxiedHttpClient(String host, Integer port)
			throws Exception {
		HttpClient client = new DefaultHttpClient(
				GooglePlayAPI.getConnectionManager());
		client.getConnectionManager().getSchemeRegistry()
				.register(Utils.getMockedScheme());
		HttpHost proxy = new HttpHost(host, port);
		client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
		return client;
	}

	private void download(String packageName) throws IOException {
		DetailsResponse details = service.details(packageName);
		AppDetails appDetails = details.getDocV2().getDetails().getAppDetails();
		Offer offer = details.getDocV2().getOffer(0);

		int versionCode = appDetails.getVersionCode();
		long installationSize = appDetails.getInstallationSize();
		int offerType = offer.getOfferType();
		boolean checkoutRequired = offer.getCheckoutFlowRequired();

		// paid application...ignore
		if (checkoutRequired) {
			System.out.println("Checkout required! Ignoring.."
					+ appDetails.getPackageName());
			return;
		}

		System.out.println("Downloading..." + appDetails.getPackageName()
				+ " : " + installationSize + " bytes");
		InputStream downloadStream = service.download(
				appDetails.getPackageName(), versionCode, offerType);

		FileOutputStream outputStream = new FileOutputStream(
				appDetails.getPackageName() + ".apk");

		byte buffer[] = new byte[1024];
		for (int k = 0; (k = downloadStream.read(buffer)) != -1;) {
			outputStream.write(buffer, 0, k);
		}
		downloadStream.close();
		outputStream.close();
		System.out.println("Downloaded! " + appDetails.getPackageName()
				+ ".apk");
	}

}

class ReviewSort implements ArgumentType<Object> {

	@Override
	public Object convert(ArgumentParser parser, Argument arg, String value)
			throws ArgumentParserException {
		try {
			return REVIEW_SORT.valueOf(value);
		} catch (IllegalArgumentException ex) {
			return value;
		}
	}
}

class ReviewSortChoice extends CollectionArgumentChoice<REVIEW_SORT> {

	public ReviewSortChoice() {
		super(REVIEW_SORT.NEWEST, REVIEW_SORT.HIGHRATING, REVIEW_SORT.HELPFUL);
	}

	@Override
	public boolean contains(Object val) {
		try {
			return super.contains(val);
		} catch (IllegalArgumentException ex) {
			return false;
		}
	}
}

class RecommendationType implements ArgumentType<Object> {

	@Override
	public Object convert(ArgumentParser parser, Argument arg, String value)
			throws ArgumentParserException {
		try {
			return RECOMMENDATION_TYPE.valueOf(value);
		} catch (IllegalArgumentException ex) {
			return value;
		}
	}
}

class ReleationChoice extends CollectionArgumentChoice<RECOMMENDATION_TYPE> {

	public ReleationChoice() {
		super(RECOMMENDATION_TYPE.ALSO_VIEWED,
				RECOMMENDATION_TYPE.ALSO_INSTALLED);
	}

	@Override
	public boolean contains(Object val) {
		try {
			return super.contains(val);
		} catch (IllegalArgumentException ex) {
			return false;
		}
	}
}

class StringJoiner {
	private String delimeter;
	List<String> elements = new ArrayList<String>();

	public StringJoiner(String delimeter) {
		this.delimeter = delimeter;
	}

	public StringJoiner add(String elem) {
		elements.add(elem);
		return this;
	}

	@Override
	public String toString() {
		if (elements.isEmpty())
			return "";
		Iterator<String> iter = elements.iterator();
		StringBuilder builder = new StringBuilder(iter.next());
		while (iter.hasNext()) {
			builder.append(delimeter).append(iter.next());
		}
		return builder.toString();
	}
}

class LoginResponseFilter extends MessageFilter<LoginResponse> {
	private String id;

	public LoginResponseFilter(String id) {
		super(LoginResponse.class);
		this.id = id;
	}

	@Override
	protected boolean accept(LoginResponse message) {
		return id.equals(message.getPacketid());
	}
}

class BindAccountResponseFilter extends MessageFilter<BindAccountResponse> {
	private String id;

	public BindAccountResponseFilter(String id) {
		super(BindAccountResponse.class);
		this.id = id;
	}

	@Override
	protected boolean accept(BindAccountResponse message) {
		return id.equals(message.getPacketid());
	}
}
